package org.thanos.bacnet.sero.warp;

import org.thanos.bacnet.sero.ScheduledExecutorServiceVariablePool;

import java.time.Clock;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Delayed;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class WarpScheduledExecutorService implements ScheduledExecutorService, ClockListener {
    private final WarpClock clock;
    private final ExecutorService executorService;
    private final ScheduledExecutorServiceVariablePool delegate;
    private final List<WarpScheduledExecutorService.ScheduleFutureImpl<?>> tasks = new ArrayList<>();
    private boolean shutdown;

    public WarpScheduledExecutorService(Clock clock) {
        if (clock instanceof WarpClock) {
            this.clock = (WarpClock)clock;
            this.clock.addListener(this);
            this.executorService = Executors.newCachedThreadPool();
            this.delegate = null;
        } else {
            this.clock = null;
            this.executorService = null;
            this.delegate = new ScheduledExecutorServiceVariablePool(clock);
        }

    }

    public void clockUpdate(LocalDateTime dateTime) {
        while(true) {
            WarpScheduledExecutorService.ScheduleFutureImpl task;
            synchronized(this.tasks) {
                label39: {
                    if (!this.tasks.isEmpty()) {
                        task = this.tasks.get(0);
                        long waitTime = task.getDelay(TimeUnit.MILLISECONDS);
                        if (waitTime <= 0L) {
                            this.tasks.remove(0);
                            break label39;
                        }
                    }

                    return;
                }
            }

            if (!task.isCancelled()) {
                task.execute();
            }
        }
    }

    public void shutdown() {
        if (this.delegate == null) {
            this.executorService.shutdown();
            this.clock.removeListener(this);
            this.shutdown = true;
        } else {
            this.delegate.shutdown();
        }

    }

    public List<Runnable> shutdownNow() {
        if (this.delegate != null) {
            return this.delegate.shutdownNow();
        } else {
            this.executorService.shutdownNow();
            this.clock.removeListener(this);
            this.shutdown = true;
            List<Runnable> runnables = new ArrayList<>(this.tasks.size());

            for (ScheduleFutureImpl<?> scheduleFuture : this.tasks) {
                runnables.add(scheduleFuture.getRunnable());
            }

            return runnables;
        }
    }

    public boolean isShutdown() {
        return this.delegate == null ? this.shutdown : this.delegate.isShutdown();
    }

    public boolean isTerminated() {
        return this.delegate == null ? this.shutdown : this.delegate.isTerminated();
    }

    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
        return this.delegate == null ? this.executorService.awaitTermination(timeout, unit) : this.delegate.awaitTermination(timeout, unit);
    }

    public <T> Future<T> submit(Callable<T> task) {
        return this.delegate == null ? this.executorService.submit(task) : this.delegate.submit(task);
    }

    public <T> Future<T> submit(Runnable task, T result) {
        return this.delegate == null ? this.executorService.submit(task, result) : this.delegate.submit(task, result);
    }

    public Future<?> submit(Runnable task) {
        return this.delegate == null ? this.executorService.submit(task) : this.delegate.submit(task);
    }

    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
        return this.delegate == null ? this.executorService.invokeAll(tasks) : this.delegate.invokeAll(tasks);
    }

    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException {
        return this.delegate == null ? this.executorService.invokeAll(tasks, timeout, unit) : this.delegate.invokeAll(tasks, timeout, unit);
    }

    public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
        return this.delegate == null ? this.executorService.invokeAny(tasks) : this.delegate.invokeAny(tasks);
    }

    public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        return this.delegate == null ? this.executorService.invokeAny(tasks, timeout, unit) : this.delegate.invokeAny(tasks, timeout, unit);
    }

    public void execute(Runnable command) {
        if (this.delegate == null) {
            this.executorService.execute(command);
        } else {
            this.delegate.execute(command);
        }

    }

    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
        return this.delegate == null ? this.addTask(new OneTime(command, delay, unit)) : this.delegate.schedule(command, delay, unit);
    }

    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
        return this.delegate == null ? this.addTask(new OneTimeCallable(callable, delay, unit)) : this.delegate.schedule(callable, delay, unit);
    }

    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
        return this.delegate == null ? this.addTask(new FixedRate(command, initialDelay, period, unit)) : this.delegate.scheduleAtFixedRate(command, initialDelay, period, unit);
    }

    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
        return this.delegate == null ? this.addTask(new FixedDelay(command, initialDelay, delay, unit)) : this.delegate.scheduleWithFixedDelay(command, initialDelay, delay, unit);
    }

    private <V> WarpScheduledExecutorService.ScheduleFutureImpl<V> addTask(WarpScheduledExecutorService.ScheduleFutureImpl<V> task) {
        synchronized(this.tasks) {
            if (task.getDelay(TimeUnit.MILLISECONDS) <= 0L) {
                this.executorService.submit(task.getRunnable());
            } else {
                int index = Collections.binarySearch(this.tasks, task);
                if (index < 0) {
                    index = -index - 1;
                }

                this.tasks.add(index, task);
            }

            return task;
        }
    }

    class OneTimeCallable<V> extends WarpScheduledExecutorService.ScheduleFutureImpl<V> {
        private final Callable<V> command;
        private final long runtime;

        public OneTimeCallable(Callable<V> command, long delay, TimeUnit unit) {
            super();
            this.command = command;
            this.runtime = WarpScheduledExecutorService.this.clock.millis() + unit.toMillis(delay);
        }

        Runnable getRunnable() {
            return () -> {
                try {
                    this.command.call();
                } catch (Exception var2) {
                    throw new RuntimeException(var2);
                }
            };
        }

        void executeImpl() {
            try {
                this.success(this.command.call());
            } catch (Exception var2) {
                this.exception(var2);
            }

        }

        public long getDelay(TimeUnit unit) {
            long millis = this.runtime - WarpScheduledExecutorService.this.clock.millis();
            return unit.convert(millis, TimeUnit.MILLISECONDS);
        }
    }

    class FixedDelay extends WarpScheduledExecutorService.Repeating {
        private final long delay;

        public FixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
            super(command, initialDelay, unit);
            this.delay = delay;
        }

        void updateNextRuntime() {
            this.nextRuntime = WarpScheduledExecutorService.this.clock.millis() + this.unit.toMillis(this.delay);
        }
    }

    class FixedRate extends WarpScheduledExecutorService.Repeating {
        private final long period;

        public FixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
            super(command, initialDelay, unit);
            this.period = period;
        }

        void updateNextRuntime() {
            this.nextRuntime += this.unit.toMillis(this.period);
        }
    }

    abstract class Repeating extends WarpScheduledExecutorService.ScheduleFutureImpl<Void> {
        private final Runnable command;
        protected final TimeUnit unit;
        protected long nextRuntime;

        public Repeating(Runnable command, long initialDelay, TimeUnit unit) {
            super();
            this.command = () -> {
                command.run();
                if (!this.isCancelled()) {
                    this.updateNextRuntime();
                    WarpScheduledExecutorService.this.addTask(this);
                }

            };
            this.nextRuntime = WarpScheduledExecutorService.this.clock.millis() + unit.toMillis(initialDelay);
            this.unit = unit;
        }

        Runnable getRunnable() {
            return this.command;
        }

        void executeImpl() {
            this.command.run();
        }

        public long getDelay(TimeUnit unit) {
            long millis = this.nextRuntime - WarpScheduledExecutorService.this.clock.millis();
            return unit.convert(millis, TimeUnit.MILLISECONDS);
        }

        public boolean isDone() {
            return this.isCancelled();
        }

        abstract void updateNextRuntime();
    }

    class OneTime extends WarpScheduledExecutorService.ScheduleFutureImpl<Void> {
        private final Runnable command;
        private final long runtime;

        public OneTime(Runnable command, long delay, TimeUnit unit) {
            super();
            this.command = command;
            this.runtime = WarpScheduledExecutorService.this.clock.millis() + unit.toMillis(delay);
        }

        Runnable getRunnable() {
            return this.command;
        }

        void executeImpl() {
            this.command.run();
            this.success(null);
        }

        public long getDelay(TimeUnit unit) {
            long millis = this.runtime - WarpScheduledExecutorService.this.clock.millis();
            return unit.convert(millis, TimeUnit.MILLISECONDS);
        }
    }

    abstract class ScheduleFutureImpl<V> implements ScheduledFuture<V> {
        private volatile boolean success;
        private volatile V result;
        private volatile Exception exception;
        private volatile boolean cancelled;
        private volatile boolean done;

        ScheduleFutureImpl() {
        }

        void execute() {
            WarpScheduledExecutorService.this.executorService.submit(() -> {
                this.executeImpl();
            });
        }

        abstract void executeImpl();

        abstract Runnable getRunnable();

        public int compareTo(Delayed that) {
            return Long.compare(this.getDelay(TimeUnit.MILLISECONDS), that.getDelay(TimeUnit.MILLISECONDS));
        }

        public boolean cancel(boolean mayInterruptIfRunning) {
            synchronized(this) {
                if (!this.done) {
                    this.cancelled = true;
                    this.notifyAll();
                    this.done = true;
                    return true;
                } else {
                    return false;
                }
            }
        }

        public boolean isCancelled() {
            synchronized(this) {
                return this.cancelled;
            }
        }

        public V get() throws InterruptedException, ExecutionException {
            try {
                return this.await(false, 0L);
            } catch (TimeoutException var2) {
                throw new RuntimeException(var2);
            }
        }

        public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
            return this.await(true, unit.toMillis(timeout));
        }

        private V await(boolean timed, long millis) throws InterruptedException, ExecutionException, TimeoutException {
            long expiry = WarpScheduledExecutorService.this.clock.millis() + millis;

            while(true) {
                synchronized(this) {
                    long remaining = expiry - WarpScheduledExecutorService.this.clock.millis();
                    if (this.success) {
                        return this.result;
                    }

                    if (this.exception != null) {
                        throw new ExecutionException(this.exception);
                    }

                    if (this.isCancelled()) {
                        throw new CancellationException();
                    }

                    if (timed) {
                        if (remaining <= 0L) {
                            throw new TimeoutException();
                        }

                        WarpUtils.wait(WarpScheduledExecutorService.this.clock, this, remaining, TimeUnit.MILLISECONDS);
                    } else {
                        this.wait();
                    }
                }
            }
        }

        public boolean isDone() {
            return this.done;
        }

        protected void success(V result) {
            synchronized(this) {
                if (!this.done) {
                    this.success = true;
                    this.result = result;
                    this.notifyAll();
                    this.done = true;
                }

            }
        }

        protected void exception(Exception exception) {
            synchronized(this) {
                if (!this.done) {
                    this.exception = exception;
                    this.notifyAll();
                    this.done = true;
                }

            }
        }
    }
}

